UBU · Aplicaciones de Bases de Datos · Tema 1

6.2.3 — Mantenimiento de la Serializabilidad

¿Cuál es el problema?

El aislamiento de instantánea (MVCC) gestiona bien los conflictos lectura-escritura. Pero tiene un punto ciego: los conflictos escritura-escritura.

✓ MVCC resuelve solo

Lectura-escritura
T lee un dato que T' está modificando → MVCC devuelve la versión correcta del snapshot. Sin esperas, sin error.

✗ MVCC necesita ayuda

Escritura-escritura
T y T' modifican el mismo dato concurrentemente → MVCC por sí solo no puede garantizar la serializabilidad. Se necesita una estrategia adicional.

Dos estrategias para resolver conflictos escritura-escritura

1. Primer commit gana (teórico)

Al hacer COMMIT, T comprueba si T' ya commitó sobre el mismo dato. Si sí → rollback implícito de T.
2. Primera actualización gana (SGBDs comerciales)

T mantiene un bloqueo de escritura hasta el fin. El conflicto se resuelve en favor de quien bloqueó primero.

Oracle y PostgreSQL usan Primera actualización gana. SQL-Server con snapshot isolation usa Primer commit gana.

Ejemplo motivador — Reserva de asientos

Dos operadores intentan reservar el mismo asiento 10 del avión X para clientes distintos al mismo tiempo. Sin control → actualización perdida: ambos creen haber reservado con éxito pero un cliente se queda sin asiento. Con Primera actualización gana, el segundo recibe un error y puede reintentar.

T1 — reserva Pepe T2 — reserva Juan → ambos quieren escribir el asiento 10

Caso 1 — Dato sin bloquear

T quiere escribir un dato que no está bloqueado por ninguna otra transacción activa. ¿Puede escribir libremente?

Regla: T adquiere el bloqueo. Luego comprueba si alguna T' concurrente ya hizo commit sobre ese dato. Si ts_fin(T') > ts_inicio(T) → T fue "adelantada" → rollback implícito de T.
Paso 1 / 5
fila X
dato compartido
Paso
T1 (serializable)
T2 (serializable)

Caso 2 — Dato bloqueado

T quiere escribir un dato que ya está bloqueado por una transacción activa T'. T debe esperar. Lo que pase después depende de cómo acabe T'.

Si T' termina con ROLLBACK
T' libera el bloqueo. T entra al Caso 1 y continúa normalmente.
Si T' termina con COMMIT
T' libera el bloqueo. T aborta con rollback implícito y lanza un error (ORA-08177 / SQL:40001).
Paso 1 / 6
Paso
T1 (serializable) — reserva Pepe
T2 (serializable) — reserva Juan

Subapartados 6.2.3.a – d

6.2.3.a — Conflictos de escritura con distintos niveles de aislamiento

Solo las transacciones SERIALIZABLE participan en la detección de conflictos de escritura. Una transacción READ COMMITTED nunca provoca rollback por este motivo.

Cuando una transacción SERIALIZABLE detecta un conflicto, actúa como si todas las concurrentes también fueran serializables, independientemente de su nivel real de aislamiento.

T1 hace COMMIT primero
→ T1 no recibe ningún error (fue la primera en confirmar).
T2 SERIALIZABLE intenta commit después
→ Detecta conflicto → ORA-08177 / SQL:40001.

6.2.3.b — Conflicto al borrar la misma fila

Dos transacciones leen una fila y, en base a su valor, ambas deciden borrarla. Esto genera un ciclo en el grafo de precedencia:

T1 → T2  (T1 debe leer antes de que T2 borre)
T2 → T1  (T2 debe leer antes de que T1 borre)
→ Ciclo → NO serializable

La segunda transacción espera, y si la primera hizo COMMIT, recibe el error de serialización.

6.2.3.c — Conflicto entre campos distintos de la misma fila

Aunque T1 modifique campo1 y T2 modifique campo2 de la misma fila, los SGBDs usan granularidad de fila. El conflicto se detecta igualmente → mismo error.

Esto previene la actualización perdida en aplicaciones que hacen UPDATE volcando todo el formulario. Si dos usuarios editan datos distintos del mismo registro y uno no ve los cambios del otro → el segundo sobrescribe sin querer el cambio del primero.

UPDATE clientes SET telefono='...', email='...' WHERE id=1;
-- T1 cambió teléfono, T2 cambia email pero vuelca el teléfono viejo

6.2.3.d — Actualizaciones perdidas con READ COMMITTED

Cuando no se puede usar SERIALIZABLE, hay dos alternativas para evitar la actualización perdida:

🔒 Bloqueo Pesimista

SELECT ... FOR UPDATE bloquea la fila antes de que el usuario la edite. Nadie más puede modificarla mientras el usuario trabaja. Garantía total pero puede generar esperas largas.
🎯 Bloqueo Optimista

No se bloquea al leer. En el momento del UPDATE se comprueba un campo version o ts. Si la fila cambió desde que se leyó → se informa al usuario y se reintenta. Menos esperas, más reintentos.

6.2.3.b — Conflicto al borrar la misma fila

Dos transacciones leen una fila, comprueban su valor y ambas deciden borrarla en función de ese valor. El mecanismo de detección del conflicto es idéntico al Caso 2, pero el razonamiento sobre por qué es un problema es más sutil.

Estado inicial de la tabla

-- mi_tabla
id=1 | campo1 = NULL

Lógica de cada transacción: "Si campo1 es NULL, borrar la fila." Como campo1 es NULL, ambas transacciones decidirán borrar.

¿Por qué no es serializable?

Si se ejecutaran en serie, la segunda transacción no encontraría la fila (ya la borró la primera) y no podría ni leer el valor para decidir. El resultado sería diferente al de la ejecución concurrente → la planificación concurrente no es equivalente a ninguna serie.

T1 → T2
T1 tiene que leer campo1 antes de que T2 lo borre/escriba
T2 → T1
T2 tiene que leer campo1 antes de que T1 lo borre/escriba
→ Ciclo en el grafo de precedencia → NO serializable
Paso 1 / 6
mi_tabla
id=1 | NULL
estado de la fila
T1 ve:
T2 ve:
Lock: libre
Paso
T1 (serializable)
T2 (serializable)
Clave: Si T2 hubiera podido borrar sin conflicto, habría intentado borrar una fila que ya no existe o que ya fue procesada. No habría error visible, pero el comportamiento sería incorrecto. El SGBD detecta el conflicto de escritura sobre la misma fila independientemente de si la operación es UPDATE o DELETE.

Bloqueo Pesimista vs. Bloqueo Optimista

Cuando usamos READ COMMITTED (el nivel por defecto en la mayoría de SGBDs) el SGBD no detecta automáticamente el problema de la actualización perdida. Hay que resolverlo a mano. Existen dos estrategias opuestas.

🔒 Bloqueo Pesimista

Filosofía: "Asumo que habrá conflicto, así que bloqueo antes de que ocurra."

Se usa SELECT ... FOR UPDATE para bloquear la fila en el momento de leerla, antes de que el usuario empiece a editar. Nadie más puede modificar la fila hasta que la transacción termine.

Ventaja: Garantía total. Imposible que haya actualización perdida.
Inconveniente: Genera esperas. Si el usuario tarda en guardar, otros quedan bloqueados.

🎯 Bloqueo Optimista

Filosofía: "Asumo que no habrá conflicto, leo sin bloquear y compruebo al guardar."

Se lee sin bloqueo. Se guarda un campo version (o timestamp). En el UPDATE se añade una condición: WHERE id=1 AND version=X. Si la fila cambió desde que se leyó, el UPDATE afecta 0 filas → se detecta el conflicto y se informa al usuario.

Ventaja: Sin esperas. Alta concurrencia.
Inconveniente: El usuario puede perder su trabajo si hubo conflicto y debe reintentar.
Escenario: Dos usuarios editan el mismo cliente. El primero usa FOR UPDATE al abrir el formulario, bloqueando la fila. El segundo usuario queda en espera hasta que el primero guarde.
Paso 1 / 6
clientes
id=1 | tel=111 | email=a@b.c
estado actual
Usuario 1:
Usuario 2:
Paso
Usuario 1 — cambia teléfono
Usuario 2 — cambia email

¿Cuándo usar cada estrategia?

🔒 Usa Pesimista cuando…
La probabilidad de conflicto es alta (muchos usuarios editan los mismos registros), o cuando el coste de reintentar es grande (operaciones largas, datos críticos).
🎯 Usa Optimista cuando…
Los conflictos son raros (usuarios distintos suelen editar registros distintos), o cuando el tiempo de edición puede ser largo y no quieres mantener bloqueos abiertos.

Quiz — 6.2.3

Puntuación: 0 / 0